package java2d.game.sudoku;

import cn.hutool.core.thread.ThreadUtil;
import com.formdev.flatlaf.fonts.roboto_mono.FlatRobotoMonoFont;
import net.miginfocom.swing.MigLayout;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class SudokuBoard extends JPanel
    implements SudokuModel.Listener
{
    private SudokuModel model;
    private SudokuCell[][] cells;
    private SudokuHintCell[] hintCells;
    private JLabel timerLabel;
    private JLabel difficultyLabel;
    private JPanel gridPanel;
    private JPanel hintPanel;
    private Timer timer;
    private Timer timerX;

    private SudokuCell selectedCell;
    private long timerStartPoint;
    private long elapsedTime;
    private boolean showCandidateNumbers;

    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    private Future<?> initTask = null;

    private static final Cursor NORMAL_CURSOR = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
    private static final Cursor WAITING_CURSOR = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
    private static final MouseAdapter EMPTY_MOUSE_ADAPTER = new MouseAdapter()
    {
        @Override
        public void mouseClicked(MouseEvent e)
        {
            e.consume();
        }
    };
    private static final KeyAdapter EMPTY_KEY_ADAPTER = new KeyAdapter()
    {
        @Override
        public void keyTyped(KeyEvent e)
        {
            e.consume();
        }

        @Override
        public void keyPressed(KeyEvent e)
        {
            e.consume();
        }

        @Override
        public void keyReleased(KeyEvent e)
        {
            e.consume();
        }
    };

    public SudokuBoard()
    {
        model = SudokuModel.standard();

        setupUI();
        setupKeyBindings();

        updateInitials();
        updateBoard();
    }

    private void setupUI()
    {
        difficultyLabel = new JLabel();
        timerLabel = new JLabel("00:00");
        gridPanel = new JPanel();
        hintPanel = new JPanel();
        timer = new Timer(100, this::timerUpdated);
        timerX = new Timer(3000, this::stopGeneration);
        timerX.setRepeats(false);

        difficultyLabel.setFont(UIManager.getFont("Label.font").deriveFont(Font.PLAIN, 40));
        timerLabel.setFont(new Font(FlatRobotoMonoFont.FAMILY, Font.PLAIN, 56));

        JToolBar headerPanel = new JToolBar();
        headerPanel.add(Box.createHorizontalStrut(5));
        headerPanel.add(difficultyLabel);
        headerPanel.add(Box.createHorizontalGlue());
        headerPanel.add(timerLabel);
        headerPanel.add(Box.createHorizontalStrut(5));

        setLayout(new MigLayout("", "[pref!]", "[pref!]2[pref!]2[pref!]"));
        add(headerPanel, "gapy 20, grow, wrap");
        add(gridPanel, "center, wrap");
        add(hintPanel, "center");
    }

    private void setupKeyBindings()
    {
        // 只有在可以获得焦点的状态下才能接收键盘事件
        setFocusable(true);

        InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "move-up");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "move-down");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "move-left");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "move-right");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "move-up");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0), "move-down");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), "move-left");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), "move-right");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "quit");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_1, KeyEvent.ALT_DOWN_MASK), "sudoku-medium");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_2, KeyEvent.ALT_DOWN_MASK), "sudoku-standard");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_3, KeyEvent.ALT_DOWN_MASK), "sudoku-abnormity");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_1, 0), "input-1");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_2, 0), "input-2");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_3, 0), "input-3");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_4, 0), "input-4");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_5, 0), "input-5");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_6, 0), "input-6");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_7, 0), "input-7");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_8, 0), "input-8");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_9, 0), "input-9");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_1, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-1");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_2, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-2");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_3, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-3");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_4, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-4");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_5, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-5");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_6, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-6");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_7, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-7");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_8, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-8");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_9, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-9");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, 0), "input-1");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, 0), "input-2");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, 0), "input-3");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, 0), "input-4");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0), "input-5");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, 0), "input-6");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, 0), "input-7");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, 0), "input-8");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, 0), "input-9");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-1");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-2");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-3");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-4");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-5");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-6");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-7");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-8");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, KeyEvent.CTRL_DOWN_MASK), "toggle-candidate-9");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_H, 0), "toggle-hints");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK), "update-cell-hints");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), "update-board-hints");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0), "clear-input");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK), "clear-cell-candidate");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "toggle-timer");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), "restart-timer");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK), "undo-input");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK), "reset-input");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK), "save-input");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_DOWN_MASK), "load-input");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_G, KeyEvent.CTRL_DOWN_MASK), "give-up");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_DOWN_MASK), "solve");

        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0), "show-help");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "restart-hard");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "restart-very-hard");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0), "restart-extreme");
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "restart-custom");

        // 不要在闭包（lambda表达式）中包含外部变量，否则后续的变化无法体现！！！

        registerAction("move-up", () -> moveCellSelection("UP"));
        registerAction("move-down", () -> moveCellSelection("DOWN"));
        registerAction("move-left", () -> moveCellSelection("LEFT"));
        registerAction("move-right", () -> moveCellSelection("RIGHT"));
        registerAction("quit", () -> System.exit(0));

        registerAction("sudoku-medium", () -> restartGame(SudokuModel.medium()));
        registerAction("sudoku-standard", () -> restartGame(SudokuModel.standard()));
        registerAction("sudoku-abnormity", () -> restartGame(SudokuModel.abnormtiy()));

        registerAction("input-1", () -> setCellNumber(1));
        registerAction("input-2", () -> setCellNumber(2));
        registerAction("input-3", () -> setCellNumber(3));
        registerAction("input-4", () -> setCellNumber(4));
        registerAction("input-5", () -> setCellNumber(5));
        registerAction("input-6", () -> setCellNumber(6));
        registerAction("input-7", () -> setCellNumber(7));
        registerAction("input-8", () -> setCellNumber(8));
        registerAction("input-9", () -> setCellNumber(9));
        registerAction("toggle-candidate-1", () -> toggleCellCandidate(1));
        registerAction("toggle-candidate-2", () -> toggleCellCandidate(2));
        registerAction("toggle-candidate-3", () -> toggleCellCandidate(3));
        registerAction("toggle-candidate-4", () -> toggleCellCandidate(4));
        registerAction("toggle-candidate-5", () -> toggleCellCandidate(5));
        registerAction("toggle-candidate-6", () -> toggleCellCandidate(6));
        registerAction("toggle-candidate-7", () -> toggleCellCandidate(7));
        registerAction("toggle-candidate-8", () -> toggleCellCandidate(8));
        registerAction("toggle-candidate-9", () -> toggleCellCandidate(9));

        registerAction("toggle-hints", this::toggleHints);
        registerAction("update-cell-hints", this::updateCellHints);
        registerAction("update-board-hints", this::updateBoardHints);
        registerAction("clear-input", this::clearCellNumber);
        registerAction("clear-cell-candidate", this::clearCellCandidate);

        registerAction("toggle-timer", this::toggleTimer);
        registerAction("restart-timer", this::restartTimer);

        registerAction("undo-input", this::undoInput);
        registerAction("save-input", this::saveInput);
        registerAction("load-input", this::loadInput);
        registerAction("reset-input", this::resetInput);

        registerAction("restart-hard", () -> restartWithDifficulty(SudokuModel.Difficulty.HARD));
        registerAction("restart-very-hard", () -> restartWithDifficulty(SudokuModel.Difficulty.VERY_HARD));
        registerAction("restart-extreme", () -> restartWithDifficulty(SudokuModel.Difficulty.EXTREME_HARD));
        registerAction("restart-custom", this::restartWithBlank);

        registerAction("solve", this::solve);
        registerAction("give-up", this::giveUp);

        registerAction("show-help", this::showHelp);
    }

    private void registerAction(String key, Runnable action)
    {
        getActionMap().put(key, new AbstractAction()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                action.run();
            }
        });
    }

    private void updateInitials()
    {
        cells = new SudokuCell[model.getScale()][model.getScale()];
        hintCells = new SudokuHintCell[model.getScale()];

        for (int ri = 0, rows = model.getScale(); ri < rows; ri++)
        {
            for (int ci = 0, columns = model.getScale(); ci < columns; ci++)
                cells[ri][ci] = new SudokuCell(this, ri, ci);
            hintCells[ri] = new SudokuHintCell(this, ri + 1);
        }

        selectedCell = null;
        showCandidateNumbers = false;
        model.addListener(this);
        timerStartPoint = 0;
        elapsedTime = 0;
    }

    private void updateBoard()
    {
        // 根据网格自动布局，并添加适当的间距
        int nGridRow = model.getScale() / model.getGridRowScale();
        int nGridCol = model.getScale() / model.getGridColumnScale();
        String colUnit = "[pref!]".repeat(model.getGridColumnScale());
        String rowUnit = "[pref!]".repeat(model.getGridRowScale());

        gridPanel.removeAll();
        gridPanel.setLayout(new MigLayout(String.format("gap 0.5, wrap %d", model.getScale()),
                                          (colUnit + "1.5").repeat(nGridCol - 1) + colUnit,
                                          (rowUnit + "1.5").repeat(nGridRow - 1) + rowUnit));
        for (int ri = 0, rows = model.getScale(); ri < rows; ri++)
            for (int ci = 0, columns = model.getScale(); ci < columns; ci++)
                gridPanel.add(cells[ri][ci]);

        hintPanel.removeAll();
        hintPanel.setLayout(new MigLayout(String.format("wrap %d", model.getScale()),
                                          "[pref!]20".repeat(model.getScale() - 1) + "[pref!]",
                                          "[pref!]"));
        for (int ni = 0, numbers = model.getScale(); ni < numbers; ni++)
            hintPanel.add(hintCells[ni]);

        Window window = SwingUtilities.getWindowAncestor(this);
        if (window instanceof JFrame frame)
        {
            frame.revalidate();
            frame.repaint();
            frame.pack();
        }
    }

    public SudokuModel getModel()
    {
        return model;
    }

    public void selectCell(SudokuCell cell)
    {
        if (cells[cell.getRow()][cell.getColumn()] != cell)
            throw new IllegalArgumentException("错误的网格对象");

        for (int ri = 0; ri < model.getScale(); ri++)
            for (int ci = 0; ci < model.getScale(); ci++)
                cells[ri][ci].clearMarks();

        selectedCell = cell;
        selectedCell.setSelected(true);

        int input = cell.getInput();
        List<SudokuModel.Cell> sameNumberCells = model.findCells(input);
        for (SudokuModel.Cell mc : sameNumberCells)
        {
            SudokuCell sc = cells[mc.getRow()][mc.getColumn()];
            if (sc != selectedCell)
                sc.highlightsInput();
        }
        List<SudokuModel.Cell> sameCandidateCells = model.findCellsWithCandidate(input);
        for (SudokuModel.Cell mc : sameCandidateCells)
        {
            SudokuCell sc = cells[mc.getRow()][mc.getColumn()];
            sc.highlightCandidate(input);
        }
    }

    private void setCellNumber(int number)
    {
        if (selectedCell != null)
        {
            ThreadUtil.execAsync(() -> {
                selectedCell.setInput(number);
                model.check();
            });
        }
    }

    private void toggleCellCandidate(int number)
    {
        if (selectedCell != null)
        {
            ThreadUtil.execAsync(() -> selectedCell.toggleCandidate(number));
        }
    }

    private void clearCellNumber()
    {
        if (selectedCell != null)
        {
            ThreadUtil.execAsync(() -> selectedCell.clearInput());
        }
    }

    private void clearCellCandidate()
    {
        if (selectedCell != null)
        {
            ThreadUtil.execAsync(() -> selectedCell.clearCandidates());
        }
    }

    private void toggleTimer()
    {
        if (timer.isRunning())
        {
            timer.stop();
            elapsedTime = System.currentTimeMillis() - timerStartPoint;
        } else
        {
            timerStartPoint = System.currentTimeMillis() - elapsedTime;
            timer.start();
        }
    }

    private void restartTimer()
    {
        timerStartPoint = System.currentTimeMillis();
        timer.restart();
    }

    private void timerUpdated(ActionEvent e)
    {
        if (timer.isRunning())
            elapsedTime = System.currentTimeMillis() - timerStartPoint;

        long seconds = elapsedTime / 1000;
        int ss = (int) (seconds % 60);
        int mm = (int) ((seconds - ss) / 60 % 60);
        int hh = (int) (seconds / 3600);
        String clock = (hh == 0) ? String.format("%02d:%02d", mm, ss)
                                 : String.format("%d:%02d:%02d", hh, mm, ss);

        if (seconds > 1800) // > 30 min
            timerLabel.setForeground(Color.ORANGE);
        if (seconds > 3600) // > 1 hrs
            timerLabel.setForeground(Color.RED);
        timerLabel.setText(clock);
    }

    private void restartGame(SudokuModel model)
    {
        this.model.removeListener(this);
        this.model = model;
        updateInitials();
        updateBoard();

        Component glassPane = getRootPane().getGlassPane();
        glassPane.setCursor(WAITING_CURSOR);
        glassPane.addMouseListener(EMPTY_MOUSE_ADAPTER);
        glassPane.addKeyListener(EMPTY_KEY_ADAPTER);
        glassPane.setVisible(true);
        glassPane.requestFocusInWindow();

        timer.stop();
        timerX.stop();

        Future<?> lastInitTask = initTask;
        if (lastInitTask != null && !lastInitTask.isDone())
        {
            lastInitTask.cancel(true);
            model.stopGeneration();
        }

        initTask = executor.schedule(() -> {
            timerX.start();
            SudokuModel.Puzzle puzzle = model.generate(SudokuModel.Difficulty.HARD);
            model.init(puzzle);
        }, 50, TimeUnit.MILLISECONDS);
    }

    private void restartWithBlank()
    {
        Component glassPane = getRootPane().getGlassPane();
        glassPane.setCursor(WAITING_CURSOR);
        glassPane.addMouseListener(EMPTY_MOUSE_ADAPTER);
        glassPane.addKeyListener(EMPTY_KEY_ADAPTER);
        glassPane.setVisible(true);
        glassPane.requestFocusInWindow();

        timer.stop();
        timerX.stop();

        Future<?> lastInitTask = initTask;
        if (lastInitTask != null && !lastInitTask.isDone())
        {
            lastInitTask.cancel(true);
            model.stopGeneration();
        }

        initTask = executor.schedule(() -> {
            SudokuModel.Puzzle puzzle = model.generate(SudokuModel.Difficulty.CUSTOM);
            model.init(puzzle);
        }, 50, TimeUnit.MILLISECONDS);
    }

    private void restartWithDifficulty(SudokuModel.Difficulty difficulty)
    {
        Component glassPane = getRootPane().getGlassPane();
        glassPane.setCursor(WAITING_CURSOR);
        glassPane.addMouseListener(EMPTY_MOUSE_ADAPTER);
        glassPane.addKeyListener(EMPTY_KEY_ADAPTER);
        glassPane.setVisible(true);
        glassPane.requestFocusInWindow();

        timer.stop();
        timerX.stop();

        Future<?> lastInitTask = initTask;
        if (lastInitTask != null && !lastInitTask.isDone())
        {
            lastInitTask.cancel(true);
            model.stopGeneration();
        }

        initTask = executor.schedule(() -> {
            timerX.start();
            SudokuModel.Puzzle puzzle = model.generate(difficulty);
            model.init(puzzle);
        }, 50, TimeUnit.MILLISECONDS);
    }

    private void stopGeneration(ActionEvent e)
    {
        if (initTask != null && !initTask.isDone())
            model.stopGeneration();
    }

    private String eval(SudokuModel.Difficulty difficulty)
    {
        return switch (difficulty)
        {
            case CUSTOM -> "自定义";
            case EASY -> "容易";
            case MEDIUM -> "中等";
            case HARD -> "困难";
            case VERY_HARD -> "很难";
            case EXTREME_HARD -> "极难";
        };
    }

    private void moveCellSelection(String direction)
    {
        if (selectedCell == null)
        {
            selectCell(cells[0][0]);
            return;
        }

        int nextRowIndex = selectedCell.getRow();
        int nextColIndex = selectedCell.getColumn();
        switch (direction)
        {
            case "UP" ->
                // 光标（被选中的单元格）向上移动一格；列首光标跳到同列尾。
                nextRowIndex = (selectedCell.getRow() - 1 + model.getScale()) % model.getScale();
            case "DOWN" ->
                // 光标（被选中的单元格）向下移动一格；列尾光标跳到同列首。
                nextRowIndex = (selectedCell.getRow() + 1) % model.getScale();
            case "LEFT" ->
                // 光标（被选中的单元格）向左移动一格；行首光标跳至行尾。
                nextColIndex = (selectedCell.getColumn() - 1 + model.getScale()) % model.getScale();
            case "RIGHT" ->
                // 光标（被选中的单元格）向右移动一格；行尾光标跳至行首。
                nextColIndex = (selectedCell.getColumn() + 1) % model.getScale();
        }
        selectCell(cells[nextRowIndex][nextColIndex]);
    }

    private void undoInput()
    {
        model.rollback();
    }

    private void saveInput()
    {
        model.snapshot();

        ThreadUtil.execAsync(() -> {
            try
            {
                model.save(new File("board.txt"));
            } catch (IOException ex)
            {
                SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this,
                                                                               "无法保存盘面状态", "遇到错误",
                                                                               JOptionPane.ERROR_MESSAGE));
            }
        });
    }

    private void loadInput()
    {
        File history = new File("board.txt");
        if (!history.exists())
        {
            JOptionPane.showMessageDialog(this, "没有历史盘面记录");
            return;
        }

        ThreadUtil.execAsync(() -> {
            try
            {
                model.load(history);
            } catch (IOException ex)
            {
                SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this,
                                                                               "无法加载盘面记录", "出现异常",
                                                                               JOptionPane.ERROR_MESSAGE));
            }
        });
    }

    private void resetInput()
    {
        model.restore();
    }

    private void solve()
    {
        timer.stop();
        model.restore();
        model.solve();
    }

    private void giveUp()
    {
        timer.stop();

        JOptionPane.showMessageDialog(this,
                                      """
                                      别灰心，继续努力！！！
                                      Ctrl-H 可以看答案哟~~~
                                      """,
                                      "(つ´ω`)つ",
                                      JOptionPane.INFORMATION_MESSAGE);
    }

    private void toggleHints()
    {
        showCandidateNumbers = !showCandidateNumbers;
        repaint();
    }

    private void updateCellHints()
    {
        if (selectedCell != null)
            model.fillCellCandidates(selectedCell.getRow(), selectedCell.getColumn());
    }

    private void updateBoardHints()
    {
        model.fillBoardCandidates();
    }

    private void showHelp()
    {
        boolean isRunning = timer.isRunning();
        timer.stop();
        long t0 = System.currentTimeMillis();
        HelpDialog dialog = new HelpDialog((Frame) SwingUtilities.getWindowAncestor(this));
        dialog.setVisible(true);
        long t1 = System.currentTimeMillis();
        if (isRunning)
        {
            timerStartPoint = timerStartPoint + (t1 - t0);
            timer.start();
        }
    }

    boolean showCandidateNumbers()
    {
        return showCandidateNumbers;
    }

    @Override
    public void puzzleInit()
    {
        SwingUtilities.invokeLater(() -> {
            Component glassPane = getRootPane().getGlassPane();
            glassPane.setCursor(NORMAL_CURSOR);
            glassPane.removeMouseListener(EMPTY_MOUSE_ADAPTER);
            glassPane.removeKeyListener(EMPTY_KEY_ADAPTER);
            glassPane.setVisible(false);

            requestFocusInWindow();

            difficultyLabel.setText(eval(model.getDifficulty()));
            timerLabel.setText("00:00");
            timerLabel.setForeground(UIManager.getColor("Label.foreground"));
            showCandidateNumbers = false;
            repaint();

            if (initTask != null && initTask.isDone() &&
                model.getDifficulty() != SudokuModel.Difficulty.CUSTOM && !model.hasUniqueSolution())
                JOptionPane.showMessageDialog(this, "本局可能没有唯一解，请谨慎");

            if (model.getDifficulty() != SudokuModel.Difficulty.CUSTOM)
            {
                timerStartPoint = System.currentTimeMillis();
                timer.restart();
            }
        });
    }

    @Override
    public void puzzleUpdated()
    {
        SwingUtilities.invokeLater(this::repaint);
    }

    @Override
    public void puzzleSolved()
    {
        SwingUtilities.invokeLater(() -> {
            for (int ri = 0; ri < model.getScale(); ri++)
                for (int ci = 0; ci < model.getScale(); ci++)
                    cells[ri][ci].clearMarks();

            timer.stop();
            repaint();

            JOptionPane.showMessageDialog(this, "完成");
        });
    }

    @Override
    public void puzzleUnsolvable()
    {
        SwingUtilities.invokeLater(() -> {
            for (int ri = 0; ri < model.getScale(); ri++)
                for (int ci = 0; ci < model.getScale(); ci++)
                    cells[ri][ci].clearMarks();

            timer.stop();
            JOptionPane.showMessageDialog(this,
                                          "本局无解！！！",
                                          "(〒︿〒)",
                                          JOptionPane.WARNING_MESSAGE);
        });
    }
}
